/*
 *
 * Copyright 2011, Ibrahim Arief
 * 
 * This file is part of Bambu Game Backend.
 *
 * Bambu Game Backend is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Bambu Game Backend is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Bambu Game Backend.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */

package com.appspot.bambugame.server.rest;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.appspot.bambugame.server.HangmanQuestionSuggestionCommand;
import com.appspot.bambugame.server.PlurkService;
import com.appspot.bambugame.server.data.GlobalGameConfig;
import com.appspot.bambugame.server.data.HangmanPlurkQuestion;
import com.appspot.bambugame.server.data.HangmanQuestion;
import com.appspot.bambugame.server.data.HangmanQuestionSuggestion;
import com.appspot.bambugame.server.data.PlurkPlayerDailyPoint;
import com.appspot.bambugame.server.data.ScramblePlurkAnswerers;
import com.google.appengine.api.datastore.QueryResultIterator;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.google.jplurk.Qualifier;
import com.google.jplurk.exception.PlurkException;
import com.googlecode.objectify.NotFoundException;
import com.googlecode.objectify.Objectify;
import com.googlecode.objectify.ObjectifyService;

public class HangmanGameValidatorServlet extends HttpServlet
{
    /**
     * 
     */
    private static final long serialVersionUID = -8957362320144318364L;
    
    public static final String cLastMonitoredPlurkID = "__GAME_CONFIG_LAST_MONITORED_PLURK_ID";

    public void doGet(HttpServletRequest req, HttpServletResponse resp)
    {
        try
        {
            Objectify tObjectify = ObjectifyService.begin();
            Long tScramblePlurkQuestionID = null;
            
            try
            {
                GlobalGameConfig tLastMonitoredPlurkID = tObjectify.get( GlobalGameConfig.class, GlobalGameConfig.getConfigDatastoreKey( cLastMonitoredPlurkID ) );
                tScramblePlurkQuestionID = tLastMonitoredPlurkID.getLongValue();
            }
            catch (NotFoundException ex)
            {
                tObjectify.put( GlobalGameConfig.withKey( cLastMonitoredPlurkID ).withValue( null ) );
            }
            
            if (tScramblePlurkQuestionID != null)
            {
                analyzeAliveQuestionIDResponses(tScramblePlurkQuestionID, tObjectify);
            }
            else
            {
                plurkNewQuestion();
            }
            
            handlePrivatePlurks();
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
            System.out.println("Exception " + ex.getClass() + ", " + ex.getMessage() + " : " + Arrays.deepToString( ex.getStackTrace() ));
        }
    }
    
    public void handlePrivatePlurks() throws JSONException, PlurkException
    {
        JSONObject tPrivatePlurks = PlurkService.getInstance().getPlurks( null, -1, false, false, true );
        
        System.out.println("Private plurks response : " + tPrivatePlurks);
        
        JSONArray tPrivatePlurkArray = tPrivatePlurks.getJSONArray( "plurks" );

        for (int i = 0; i < tPrivatePlurkArray.length(); i++)
        {
            JSONObject tPrivatePlurkObject = tPrivatePlurkArray.getJSONObject( i );
            handlePrivatePlurk(tPrivatePlurkObject);
        }
    }
    
    public void handlePrivatePlurk(JSONObject pPrivatePlurkObject)//, JSONObject pPlurkerDataObject)
    {
        try
        {
            String tPlurkRawContent = pPrivatePlurkObject.getString( "content_raw" ).toLowerCase().trim();
            long tPlurkID = pPrivatePlurkObject.getLong( "plurk_id" );
            
            if (tPlurkRawContent.startsWith( "!suggest" ))
            {
                JSONObject tPrivatePlurkResponse = PlurkService.getInstance().responseGet( String.valueOf( tPlurkID ) ); //getPlurkService().using( Responses.class ).get( tPlurkID );
                JSONArray tPrivatePlurkResponseArray = tPrivatePlurkResponse.getJSONArray("responses");
                
                System.out.println("Suggestion private plurk response is : " + tPrivatePlurkResponse);
                
                HangmanQuestionSuggestionCommand tSuggestionCommand = new HangmanQuestionSuggestionCommand( tPrivatePlurkResponseArray );
                
                System.out.println("Suggestion command status is : " + tSuggestionCommand.toString());
                
                if (!tSuggestionCommand.hasGreetingBeingPlurked())
                {
                    for (int i = 0; i < HangmanQuestionSuggestionCommand.cGreetingResponseStringArray.length; i++)
                    {
                        PlurkService.getInstance().responseAdd( String.valueOf (tPlurkID), HangmanQuestionSuggestionCommand.cGreetingResponseStringArray[i], Qualifier.SAYS );
                        try { Thread.sleep(100); } catch (Exception ex) {}
                    }
                }
                
                if (!tSuggestionCommand.hasSentenceOfferBeingPlurked())
                {
                    PlurkService.getInstance().responseAdd( String.valueOf (tPlurkID), HangmanQuestionSuggestionCommand.cSentenceOfferResponseString, Qualifier.SAYS );
                    return;
                }
                
                if (!tSuggestionCommand.hasQuestionBeenSubmittedByUser()) return;
                
                if (!tSuggestionCommand.isSubmittedQuestionValid())
                {
                    if (!tSuggestionCommand.hasInvalidReasonBeenPlurked())
                    {
                        // sorry, the sentence is not valid due to..
                        String tInvalidReason = HangmanQuestionSuggestionCommand.cInvalidReasonResponseString + tSuggestionCommand.getQuestionInvalidityReason();
                        PlurkService.getInstance().responseAdd( String.valueOf (tPlurkID), tInvalidReason, Qualifier.SAYS );
                        
                        try { Thread.sleep(100); } catch (Exception ex) {}
                        
                        PlurkService.getInstance().responseAdd( String.valueOf (tPlurkID), "please re-type the question by using the !question command again", Qualifier.SAYS );
                    }
                    return;
                }
                
                // at this point, the question would be valid
                
                if (!tSuggestionCommand.hasConfirmationBeenPlurked())
                {
                    // plurk confirmation
                    String tConfirmationString = HangmanQuestionSuggestionCommand.cNeedConfirmationResponseStringStart + 
                                                 "the question is " + 
                                                 tSuggestionCommand.getValidQuestionString() + 
                                                 " and with hint " + 
                                                 tSuggestionCommand.getValidHintString() + 
                                                 HangmanQuestionSuggestionCommand.cNeedConfirmationResponseStringEnd;
                    
                    PlurkService.getInstance().responseAdd( String.valueOf (tPlurkID), tConfirmationString, Qualifier.SAYS );
                    try { Thread.sleep(100); } catch (Exception ex) {}
                    PlurkService.getInstance().responseAdd( String.valueOf (tPlurkID), "or type !newquestion to cancel and re-start the question submission", Qualifier.SAYS );
                    return;
                }
                
                if (!tSuggestionCommand.hasConfirmationBeenGivenByUser())
                {
                    return;
                }
                
                // at this point, if the question is not stored yet, we should store the question given by the user
                if (!tSuggestionCommand.hasQuestionBeenAccepted())
                {
                    // store at datastore
                    String tSuggesterUserName = tPrivatePlurkResponse.getJSONObject("friends").getJSONObject( "" + tSuggestionCommand.getSubmitterID() ).getString( "nick_name" );
                    
                    HangmanQuestionSuggestion tSuggestion = new HangmanQuestionSuggestion(tSuggestionCommand.getValidQuestionString(),
                                                                                          tSuggestionCommand.getValidHintString(),
                                                                                          null, // extras
                                                                                          tSuggestionCommand.getSubmitterID(),
                                                                                          tSuggesterUserName,
                                                                                          tPlurkID);
                    
                    ObjectifyService.begin().put( tSuggestion );
                    
                    // print thank you
                    PlurkService.getInstance().responseAdd( String.valueOf (tPlurkID), HangmanQuestionSuggestionCommand.cQuestionHasBeenSubmittedResponseString, Qualifier.SAYS );
                    
                    try { Thread.sleep(100); } catch (Exception ex) {}
                    
                    PlurkService.getInstance().responseAdd( String.valueOf (tPlurkID), "you could also submit another question by typing !newquestion", Qualifier.SAYS );
                }
            }
        }
        catch (Exception ex)
        {
            System.out.println("Exception " + ex.getClass() + ", " + ex.getMessage() + " : " + Arrays.deepToString( ex.getStackTrace() ));
        }
    }
    
    public void analyzeAliveQuestionIDResponses(long pPlurkQuestionID, Objectify pObjectify) throws PlurkException, JSONException
    {
        HangmanPlurkQuestion tPlurkQuestion = pObjectify.get( HangmanPlurkQuestion.class, pPlurkQuestionID );
        
        if (tPlurkQuestion.LAST_RESPONSE_OFFSET == null) tPlurkQuestion.LAST_RESPONSE_OFFSET = 0;
        
        JSONObject tPlurkResponse = PlurkService.getInstance().responseGet( String.valueOf( tPlurkQuestion.PLURK_ID ) );
        JSONArray tResponseArray = tPlurkResponse.getJSONArray( "responses" );
        
        System.out.println("Analyzing plurk ID " + tPlurkQuestion.PLURK_ID + ", plurk received " + tResponseArray.length() + " plurk responses. Full JSON response of the plurk is " + tPlurkResponse);
        
        Set<Character> tGuessedCharacterSet = new HashSet<Character>();
        
        Long tGuesserPlurkID = null;
        boolean tIsWinBecauseOfDirectGuess = false;
        
        Set<String> tGuesserWrongGuessStringSet = new HashSet<String>();
        Set<String> tGuesserLateGuessStringSet = new HashSet<String>();
        
        System.out.println("Plurk last response offset is : " + tPlurkQuestion.LAST_RESPONSE_OFFSET);
        
        for (int i = tPlurkQuestion.LAST_RESPONSE_OFFSET; i < tResponseArray.length(); i++)
        {
            JSONObject tResponseObject = tResponseArray.getJSONObject( i );
            String tResponseAnswer = tResponseObject.getString( "content_raw" ).trim().toLowerCase();
            
            System.out.println("Validating response #" + i + ", answer from @" + tPlurkResponse.getJSONObject( "friends" ).getJSONObject( String.valueOf( tResponseObject.getLong( "user_id" ) ) ).getString( "nick_name" ) + " : " + tResponseAnswer);
            
            if (tResponseAnswer.length() == 1)
            {
                Character tGuessedCharacter = tResponseAnswer.charAt( 0 );
                
                if (Character.isLetterOrDigit( tGuessedCharacter ) && !tPlurkQuestion.hasCharacterBeenGuessed( tGuessedCharacter ))
                {
                    tGuessedCharacterSet.add( tGuessedCharacter );
                    tPlurkQuestion.addGuessedCharacter( tGuessedCharacter );
                    
                    if (tPlurkQuestion.hasQuestionBeenGuessed() && tGuesserPlurkID == null)
                    {
                        tGuesserPlurkID = tResponseObject.getLong( "user_id" );
                    }
                    if (tPlurkQuestion.hasQuestionBeenHanged())
                    {
                        break;
                    }
                }
            }
            else if (tResponseAnswer.startsWith( "!guess " ))
            {
                String tPlurkerGuessString = tResponseAnswer.substring( "!guess".length() );
                
                if (tPlurkQuestion.isGuessStringCorrect(tPlurkerGuessString) && tGuesserPlurkID == null)
                {
                    tGuesserPlurkID = tResponseObject.getLong( "user_id" );
                    tIsWinBecauseOfDirectGuess = true;
                }
                else if (tPlurkQuestion.isGuessStringCorrect(tPlurkerGuessString) && tGuesserPlurkID != null)
                {
                    String tPlurkerNameString = tPlurkResponse.getJSONObject( "friends" ).getJSONObject( String.valueOf( tResponseObject.getLong( "user_id" ) ) ).getString( "nick_name" );
                    tGuesserLateGuessStringSet.add( tPlurkerNameString + "|" + tPlurkerGuessString );
                    
                    System.out.println("Adding [" + tPlurkerNameString + "|" + tPlurkerGuessString + "] to the late map " + tGuesserLateGuessStringSet.toString());
                }
                else
                {
                    String tPlurkerNameString = tPlurkResponse.getJSONObject( "friends" ).getJSONObject( String.valueOf( tResponseObject.getLong( "user_id" ) ) ).getString( "nick_name" );
                    tGuesserWrongGuessStringSet.add( tPlurkerNameString + "|" + tPlurkerGuessString );
                    
                    System.out.println("Adding [" + tPlurkerNameString + "|" + tPlurkerGuessString + "] to the map " + tGuesserWrongGuessStringSet.toString());
                }
            }
        }
        
        if (tGuesserPlurkID != null) // we have a winner here!
        {
            JSONObject tWinnerJSONObject = tPlurkResponse.getJSONObject( "friends" ).getJSONObject( String.valueOf( tGuesserPlurkID ) );
            int tAnswererAnswerCount = incrementAnswererAnswerCount( tGuesserPlurkID, tWinnerJSONObject.getString( "nick_name" ) );
            
            if (tIsWinBecauseOfDirectGuess)
            {
                postCongratulatoryMessage(tPlurkQuestion, tWinnerJSONObject, tIsWinBecauseOfDirectGuess, tAnswererAnswerCount);
            }
            else
            {
                postCharacterGuessesResponse(tPlurkQuestion, tGuessedCharacterSet);
                
                try { Thread.sleep( 100 ); } catch (Exception ex) {}
                
                postCongratulatoryMessage(tPlurkQuestion, tWinnerJSONObject, tIsWinBecauseOfDirectGuess, tAnswererAnswerCount);
            }
            
            try { Thread.sleep( 100 ); } catch (Exception ex) {}
            
            tPlurkQuestion.IS_SOLVED = Boolean.TRUE;
            
            // reset the monitored ID
            pObjectify.put( GlobalGameConfig.withKey( cLastMonitoredPlurkID ).withValue( null ) );
            
            if (tPlurkQuestion.SENTENCE.equalsIgnoreCase( "You have been trolled" ))
            {
                PlurkService.getInstance().responseAdd( String.valueOf (tPlurkQuestion.PLURK_ID), "(troll)", Qualifier.GIVES);
            }
        }
        else if (!tGuessedCharacterSet.isEmpty()) // no winner yet, post new guesses result if someone made a guess
        {
            postCharacterGuessesResponse(tPlurkQuestion, tGuessedCharacterSet);
        }
        
        if (!tGuesserWrongGuessStringSet.isEmpty())
        {
            for (String tWrongGuess : tGuesserWrongGuessStringSet)
            {
                String tPlurkerName = tWrongGuess.substring( 0, tWrongGuess.indexOf( "|" ) );
                String tPlurkerAnswer = tWrongGuess.substring( tWrongGuess.indexOf( "|" ) + 1 ).trim();
                
                PlurkService.getInstance().responseAdd( String.valueOf (tPlurkQuestion.PLURK_ID), "sorry @" + tPlurkerName + ", the answer is not `" + tPlurkerAnswer + "`", Qualifier.SAYS );
            }
        }
        
        if (!tGuesserLateGuessStringSet.isEmpty())
        {
            for (String tLateGuess : tGuesserLateGuessStringSet)
            {
                String tPlurkerName = tLateGuess.substring( 0, tLateGuess.indexOf( "|" ) );
                String tPlurkEmote = "(troll)";
                
                if (tPlurkerName.equalsIgnoreCase( "ruicilita" )) tPlurkEmote = "(cozy)";
                
                PlurkService.getInstance().responseAdd( String.valueOf (tPlurkQuestion.PLURK_ID), "the answer is correct @" + tPlurkerName + ", but you are too late. " + tPlurkEmote, Qualifier.SAYS );
            }
        }
        
        if (tPlurkQuestion.hasQuestionBeenHanged()) // if question has been hanged
        {
            PlurkService.getInstance().responseAdd( String.valueOf (tPlurkQuestion.PLURK_ID), "Whoops, you guys ran out of tries, this question is closed now. (yarr) Remember, there are only 10 incorrect guesses allowed per question.", Qualifier.SAYS );
            
            try { Thread.sleep( 100 ); } catch (Exception ex) {}
            
            PlurkService.getInstance().responseAdd( String.valueOf (tPlurkQuestion.PLURK_ID), "*Psst.. The answer is actually '" + tPlurkQuestion.SENTENCE + "', but don't tell anyone that I told you that* [nj]", Qualifier.SAYS );
            
            try { Thread.sleep( 100 ); } catch (Exception ex) {}
            
            if (tPlurkQuestion.EXTRAS != null)
            {
                PlurkService.getInstance().responseAdd( String.valueOf (tPlurkQuestion.PLURK_ID), "(music) " + tPlurkQuestion.EXTRAS, Qualifier.SHARES );
            }
            
            // reset the monitored ID
            pObjectify.put( GlobalGameConfig.withKey( cLastMonitoredPlurkID ).withValue( null ) );
        }
        
        updatePlurkQuestionAtPlurk(tPlurkQuestion);
        
        // update last offset with the length of response
        tPlurkQuestion.LAST_RESPONSE_OFFSET = tResponseArray.length();
        
        pObjectify.put( tPlurkQuestion );
    }
    
    protected int incrementAnswererAnswerCount(long pAnswererPlurkID, String pAnswererPlurkNickName)
    {
        Objectify tObjectify = ObjectifyService.begin();
        
        ScramblePlurkAnswerers tAnswerer = null;
        
        try
        {
            tAnswerer = tObjectify.get( ScramblePlurkAnswerers.class, pAnswererPlurkID );
        }
        catch (NotFoundException ex)
        {
            // expected case
            tAnswerer = new ScramblePlurkAnswerers();
            tAnswerer.PLURK_USER_NAME = pAnswererPlurkNickName;
            tAnswerer.PLURK_USER_ID = pAnswererPlurkID;
            tAnswerer.NUMBER_OF_CORRECT_ANSWERS = 0;
        }
        
        tAnswerer.NUMBER_OF_CORRECT_ANSWERS = tAnswerer.NUMBER_OF_CORRECT_ANSWERS + 1;
        
        if (tAnswerer.PLURK_USER_NAME == null) tAnswerer.PLURK_USER_NAME = pAnswererPlurkNickName;
        if (tAnswerer.PLURK_USER_NAME != null && tAnswerer.PLURK_USER_NAME.isEmpty()) tAnswerer.PLURK_USER_NAME = pAnswererPlurkNickName;
        
        tObjectify.put( tAnswerer );
        
        // now we add to the daily point score
        PlurkPlayerDailyPoint tAnswererDailyPoint = null;
        long tyyyyMMddDate = Long.parseLong( new SimpleDateFormat( "yyyyMMdd" ).format( Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ).getTime() ) );
        long tDailyPointKey = Long.parseLong( pAnswererPlurkID + "" + tyyyyMMddDate );
        
        try
        {
            tAnswererDailyPoint = tObjectify.get( PlurkPlayerDailyPoint.class, tDailyPointKey );
        }
        catch (NotFoundException ex)
        {
            // expected
            tAnswererDailyPoint = new PlurkPlayerDailyPoint(pAnswererPlurkID, tyyyyMMddDate, pAnswererPlurkNickName);
        }
        
        tAnswererDailyPoint.POINT = tAnswererDailyPoint.POINT + 10;
        
        tObjectify.put( tAnswererDailyPoint );
        
        return tAnswerer.NUMBER_OF_CORRECT_ANSWERS;
    }
    
    public void updatePlurkQuestionAtPlurk(HangmanPlurkQuestion pPlurkQuestion) throws PlurkException
    {
        String tUpdatedQuestionSentence = getQuestionSentence( pPlurkQuestion );
        
        PlurkService.getInstance().plurkEdit( String.valueOf(pPlurkQuestion.PLURK_ID), tUpdatedQuestionSentence );
    }
    
    public void postCongratulatoryMessage(HangmanPlurkQuestion pPlurkQuestion, JSONObject pQuestionAnswererJSONObject, boolean pIsWinBecauseOfDirectGuess, int pAnswererAnswerCount) throws PlurkException, JSONException
    {
        String tQuestionAnswererNickName = pQuestionAnswererJSONObject.getString( "nick_name" );
        String tAnswererGenderString = pQuestionAnswererJSONObject.getLong( "gender" ) == 0 ? "Her" : "His";
        
        String tMessage = "congratulations to @" + tQuestionAnswererNickName + ", " + tAnswererGenderString + " direct guess is correct! :D " + tAnswererGenderString + " score is now " + (pAnswererAnswerCount * 10) + ".";
        
        if (!pIsWinBecauseOfDirectGuess)
        {
            tMessage = "congratulations to @" + tQuestionAnswererNickName + ", " + tAnswererGenderString + " letter guess completes the answer! :D " + tAnswererGenderString + " score is now " + (pAnswererAnswerCount * 10) + ".";
        }
        
        PlurkService.getInstance().responseAdd( String.valueOf (pPlurkQuestion.PLURK_ID), tMessage, Qualifier.GIVES );
        
        if (pPlurkQuestion.EXTRAS != null)
        {
            PlurkService.getInstance().responseAdd( String.valueOf (pPlurkQuestion.PLURK_ID), "(music) " + pPlurkQuestion.EXTRAS, Qualifier.SHARES );
        }
    }
    
    public void postCharacterGuessesResponse(HangmanPlurkQuestion pPlurkQuestion, Set<Character> tGuessedCharacterSet) throws PlurkException
    {
        StringBuilder tStringBuilder = new StringBuilder();
        tStringBuilder.append( "Guessed character" );
        if (tGuessedCharacterSet.size() == 1)
        {
            Character tCurrentChar = tGuessedCharacterSet.iterator().next();
            tStringBuilder.append( " is " + tCurrentChar );
            tStringBuilder.append( pPlurkQuestion.isGuessCharacterCorrect( tCurrentChar ) ? " [Correct]" : " [Incorrect]" );
        }
        else if (tGuessedCharacterSet.size() > 1)
        {
            tStringBuilder.append( "s are : " );
            
            Iterator<Character> tGuessedCharacterIterator = tGuessedCharacterSet.iterator();
            List<Character> tCorrectGuessList = new ArrayList<Character>();
            List<Character> tIncorrectGuessList = new ArrayList<Character>();
            
            while (tGuessedCharacterIterator.hasNext())
            {
                Character tCurrentChar = tGuessedCharacterIterator.next();
                if (pPlurkQuestion.isGuessCharacterCorrect( tCurrentChar ) )
                {
                    tCorrectGuessList.add( tCurrentChar );
                }
                else
                {
                    tIncorrectGuessList.add( tCurrentChar );
                }
            }
            
            if (!tCorrectGuessList.isEmpty())
            {
                for (Character tCorrectChar : tCorrectGuessList)
                {
                    tStringBuilder.append( tCorrectChar + ", ");
                }
                tStringBuilder.append( " [Correct]" );
            }
            
            if (!tIncorrectGuessList.isEmpty())
            {
                for (Character tIncorrectChar : tIncorrectGuessList)
                {
                    tStringBuilder.append( tIncorrectChar + ", ");
                }
                tStringBuilder.append( " [Incorrect]" );
            }
        }
        
        tStringBuilder.append( ". Remaining lives : [" + pPlurkQuestion.getLivesRemainingInPercent() + "]" );
        
        PlurkService.getInstance().responseAdd( String.valueOf (pPlurkQuestion.PLURK_ID), tStringBuilder.toString(), Qualifier.SAYS );
        
        try
        {
            try { Thread.sleep( 100 ); } catch (Exception ex) {}
            
            PlurkService.getInstance().responseAdd( String.valueOf (pPlurkQuestion.PLURK_ID), "current sentence is : " + getCoreQuestionSentence( pPlurkQuestion ), Qualifier.SAYS );
        }
        catch (Exception ex) 
        {
            System.out.println("Exception " + ex.getClass().getName() + " occurred, " + ex.getMessage() + " : " + Arrays.deepToString( ex.getStackTrace() ) );
        } // anti-flood protection
    }
    
    public static final String cHangmanQuestionsMemcacheKey = "HANGMAN_QUESTIONS_KEY";
    public static final String cHangmanForceReloadKey = "HANGMAN_SHOULD_FORCE_QUESTION_RELOAD_KEY";
    
    protected static MemcacheService mMemcacheService = null;
    
    @SuppressWarnings( "unchecked" )
    protected Long getRandomQuestionID()
    {
        if (mMemcacheService == null) mMemcacheService = MemcacheServiceFactory.getMemcacheService();
        
        boolean tShouldForceReload = false;
        if (mMemcacheService.contains( cHangmanForceReloadKey ) && mMemcacheService.get( cHangmanForceReloadKey ) != null)
        {
            tShouldForceReload = Boolean.parseBoolean( String.valueOf( mMemcacheService.get( cHangmanForceReloadKey ) ) );
        }
        else
        {
            mMemcacheService.put( cHangmanForceReloadKey, Boolean.FALSE );
        }
        
        List<String> tHangmanQuestionKeyCountList = null;
        if (mMemcacheService.contains( cHangmanQuestionsMemcacheKey ) && !tShouldForceReload)
        {
            tHangmanQuestionKeyCountList = (List<String>) mMemcacheService.get( cHangmanQuestionsMemcacheKey );
            System.out.println("Fetching hangman question key list from cache : " + tHangmanQuestionKeyCountList);
        }
        else
        {
            tHangmanQuestionKeyCountList = getHangmanQuestionCountList();
            mMemcacheService.put( cHangmanQuestionsMemcacheKey, tHangmanQuestionKeyCountList );
            System.out.println("Expensively fetching hangman quesstion key list from datastore : " + tHangmanQuestionKeyCountList);
            
            // reset the force reload to false
            mMemcacheService.put( cHangmanForceReloadKey, Boolean.FALSE );
        }
        
        if (tHangmanQuestionKeyCountList.isEmpty()) return null;
        
        System.out.println("Before sorting : " + tHangmanQuestionKeyCountList );
        
        Collections.sort( tHangmanQuestionKeyCountList );
        
        System.out.println("After sorting : " + tHangmanQuestionKeyCountList );
        
        List<Long> tSmallestAppearanceCountSubGroupKeyList = filterWithOnlySmallestAppearanceCountGroup( tHangmanQuestionKeyCountList );
        
        int tSelectedIndex = (int) Math.floor( Math.random() * ((double) tSmallestAppearanceCountSubGroupKeyList.size()) );
        
        String tPickedUpString = tHangmanQuestionKeyCountList.get( tSelectedIndex );
        
        System.out.println("Picked up string [" + tSelectedIndex + "] : " + tPickedUpString);
        
        int tAppearanceCount = Integer.parseInt( tPickedUpString.substring( 0, tPickedUpString.indexOf( "|" ) ) );
        long tQuestionID = Long.parseLong( tPickedUpString.substring( tPickedUpString.indexOf( "|" ) + 1 ) );
        
        tAppearanceCount++;
        
        StringBuilder tCountBuilder = new StringBuilder("" + tAppearanceCount);
        while (tCountBuilder.length() < 9) tCountBuilder.insert( 0, '0' );
        
        tHangmanQuestionKeyCountList.set( tSelectedIndex, tCountBuilder.toString() + "|" + tQuestionID );
        
        mMemcacheService.put( cHangmanQuestionsMemcacheKey, tHangmanQuestionKeyCountList ); // re-put in memcache
        
        return tQuestionID;
    }
    
    protected List<Long> filterWithOnlySmallestAppearanceCountGroup(List<String> pHangmanQuestionKeyCountList)
    {
        List<Long> tReturnList = new ArrayList<Long>();
        
        if (pHangmanQuestionKeyCountList.isEmpty()) return tReturnList;
        
        String tInitialQuestionString = pHangmanQuestionKeyCountList.get( 0 );
        int tInitialAppearanceCount = Integer.parseInt( tInitialQuestionString.substring( 0, tInitialQuestionString.indexOf( "|" ) ) );
        tReturnList.add( Long.parseLong( tInitialQuestionString.substring( tInitialQuestionString.indexOf( "|" ) + 1 ) ) );
        
        for (int i = 1; i < pHangmanQuestionKeyCountList.size(); i++)
        {
            String tHangmanQuestionKeyCountString = pHangmanQuestionKeyCountList.get( i );
            
            int tAppearanceCount = Integer.parseInt( tHangmanQuestionKeyCountString.substring( 0, tHangmanQuestionKeyCountString.indexOf( "|" ) ) );
            
            if (tAppearanceCount == tInitialAppearanceCount)
            {
                tReturnList.add( Long.parseLong( tHangmanQuestionKeyCountString.substring( tHangmanQuestionKeyCountString.indexOf( "|" ) + 1 ) ) );
            }
            else
            {
                break;
            }
        }
        
        return tReturnList;
    }
    
    protected List<String> getHangmanQuestionCountList()
    {
        Objectify tObjectify = ObjectifyService.begin();
        QueryResultIterator<HangmanQuestion> tQueryResultIterable = tObjectify.query( HangmanQuestion.class ).fetch().iterator();
        
        List<String> tCountSentenceString = new ArrayList<String>();
        while (tQueryResultIterable.hasNext())
        {
            HangmanQuestion tQuestion = tQueryResultIterable.next();
            StringBuilder tQuestionCountBuilder = new StringBuilder("" + tQuestion.APPEARANCE_COUNT);
            while (tQuestionCountBuilder.length() < 9) tQuestionCountBuilder.insert( 0, '0' );
            
            tCountSentenceString.add( tQuestionCountBuilder.toString() + "|" + tQuestion.ID );
        }
        
        return tCountSentenceString;
    }
    
    public void plurkNewQuestion() throws PlurkException, JSONException
    {
        Long tRandomQuestionID = getRandomQuestionID();
        
        if (tRandomQuestionID == null) return; // this means there are no questions in the datastore
        
        Objectify tObjectify = ObjectifyService.begin();
        HangmanQuestion tQuestion = tObjectify.get( HangmanQuestion.class, tRandomQuestionID );
        
        HangmanPlurkQuestion tPlurkQuestion = new HangmanPlurkQuestion( 0, tQuestion.SENTENCE, tQuestion.HINT, tQuestion.EXTRAS );
        
        JSONObject tAddResponse = PlurkService.getInstance().plurkAdd( getQuestionSentence( tPlurkQuestion ), Qualifier.ASKS );
        tPlurkQuestion.PLURK_ID = tAddResponse.getLong( "plurk_id" );
        tObjectify.put( tPlurkQuestion );
        
        System.out.print("Updating appearance count of question " + tQuestion.SENTENCE + " with ID " + tQuestion.ID + " from " + tQuestion.APPEARANCE_COUNT);
        
        tQuestion.APPEARANCE_COUNT = tQuestion.APPEARANCE_COUNT + 1;
        
        System.out.println(" to " + tQuestion.APPEARANCE_COUNT);
        
        tObjectify.put( tQuestion );
        tObjectify.put( GlobalGameConfig.withKey( cLastMonitoredPlurkID ).withValue( tPlurkQuestion.PLURK_ID ) );
        
        try { Thread.sleep( 100 ); } catch (Exception ex) {}
        
        // add response about how to play
        PlurkService.getInstance().responseAdd( String.valueOf (tPlurkQuestion.PLURK_ID), "write one-letter response to guess a letter. To directly guess the answer, write !guess<space><your guess>.", Qualifier.SAYS );
    }
    
    public String getCoreQuestionSentence(HangmanPlurkQuestion pPlurkQuestion)
    {
        String[] tSentenceWordArray = pPlurkQuestion.SENTENCE.trim().split( " " );
        StringBuilder tSentenceStringBuilder = new StringBuilder();
        
        for (int i = 0; i < tSentenceWordArray.length; i++)
        {
            String tSentenceWord = tSentenceWordArray[i];
            
            for (int j = 0; j < tSentenceWord.length(); j++)
            {
                Character tSentenceChar = tSentenceWord.charAt( j );
                Character tAppendedChar = pPlurkQuestion.hasCharacterBeenGuessed( tSentenceChar ) ? tSentenceChar : '_';
                
                // special handling for special characters print out
                if (HangmanPlurkQuestion.cSpeciallyAcceptedChars.contains( tSentenceChar) )
                {
                    tAppendedChar = tSentenceChar;
                }
                
                tSentenceStringBuilder.append( tAppendedChar + " " );
            }
            
            if (i < tSentenceWordArray.length - 1) // if not the last one
            {
                tSentenceStringBuilder.append( "[sp] " );
            }
        }
        
        return tSentenceStringBuilder.toString();
    }
    
    public String getQuestionSentence(HangmanPlurkQuestion pPlurkQuestion)
    {
        int tNumberOfWords = pPlurkQuestion.getNumberOfWords();
        
        StringBuilder tSentenceStringBuilder = new StringBuilder("[" + tNumberOfWords + " word" + (tNumberOfWords > 1 ? "s" : "") + "] [" + pPlurkQuestion.getLivesRemainingInPercent() + "]");
        
        tSentenceStringBuilder.append( getCoreQuestionSentence( pPlurkQuestion ) );
        
        tSentenceStringBuilder.append( " (hint: " + pPlurkQuestion.HINT + ")" );
        
        return tSentenceStringBuilder.toString();
    }
}
